﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel;
using System.Reflection;
using System.Runtime.InteropServices;

namespace System;

internal class WindowClass
{
    private static RECT DefaultBounds
        => new(PInvoke.CW_USEDEFAULT, PInvoke.CW_USEDEFAULT, PInvoke.CW_USEDEFAULT, PInvoke.CW_USEDEFAULT);

    // Stash the delegate to keep it from being collected
    private readonly WNDPROC _windowProcedure;
    private WNDCLASSW _wndClass;
    private readonly string _className;
    private readonly string _menuName;

    public ATOM Atom { get; private set; }
    public HWND MainWindow { get; private set; }
    public HINSTANCE ModuleInstance { get; }

    /// <summary>
    ///  Constructor.
    /// </summary>
    /// <param name="className">Name, or default will be generated.</param>
    /// <param name="moduleInstance">Module to associate with the window. The entry assembly is the default.</param>
    /// <param name="backgroundBrush">Use (HINSTANCE)(-1) for no background brush.</param>
    /// <param name="icon">Use (HICON)(-1) for no icon.</param>
    /// <param name="cursor">Use (HCURSOR)(-1) for no cursor.</param>
    /// <param name="menuName">Menu name, can not set with <paramref name="menuId"/>.</param>
    /// <param name="menuId">Menu id, can not set with <paramref name="menuName"/>.</param>
    [UnconditionalSuppressMessage("SingleFile", "IL3002:Avoid calling members marked with 'RequiresAssemblyFilesAttribute' when publishing as a single-file", Justification = "Test only binary and not shippable.")]
    public unsafe WindowClass(
        string className = default,
        HINSTANCE moduleInstance = default,
        WNDCLASS_STYLES classStyle = WNDCLASS_STYLES.CS_HREDRAW | WNDCLASS_STYLES.CS_VREDRAW,
        HBRUSH backgroundBrush = default,
        HICON icon = default,
        HCURSOR cursor = default,
        string menuName = null,
        int menuId = 0,
        int classExtraBytes = 0,
        int windowExtraBytes = 0)
    {
        // Handle default values
        className ??= Guid.NewGuid().ToString();

        if (backgroundBrush.IsNull)
        {
            backgroundBrush = PInvoke.GetSysColorBrush(SYS_COLOR_INDEX.COLOR_WINDOW);
        }
        else if (backgroundBrush == (HBRUSH)(-1))
        {
            backgroundBrush = default;
        }

        if (icon.IsNull)
        {
            icon = PInvokeCore.LoadIcon((HINSTANCE)0, (PCWSTR)(char*)PInvokeCore.IDI_APPLICATION);
        }
        else if (icon == (-1))
        {
            icon = default;
        }

        if (cursor == default)
        {
            cursor = PInvoke.LoadCursor((HINSTANCE)0, (PCWSTR)(char*)PInvoke.IDC_ARROW);
        }
        else if (cursor == (-1))
        {
            cursor = default;
        }

        if (moduleInstance.IsNull)
            Marshal.GetHINSTANCE(Assembly.GetCallingAssembly().Modules.First());

        if (menuId != 0 && menuName is not null)
            throw new ArgumentException($"Can't set both {nameof(menuName)} and {nameof(menuId)}.");

        _windowProcedure = WNDPROC;
        ModuleInstance = moduleInstance;

        _className = className;
        _menuName = menuName ?? string.Empty;

        _wndClass = new WNDCLASSW
        {
            style = classStyle,
            lpfnWndProc = (delegate* unmanaged[Stdcall]<HWND, uint, WPARAM, LPARAM, LRESULT>)Marshal.GetFunctionPointerForDelegate(_windowProcedure),
            cbClsExtra = classExtraBytes,
            cbWndExtra = windowExtraBytes,
            hInstance = moduleInstance,
            hIcon = icon,
            hCursor = cursor,
            hbrBackground = backgroundBrush,
            lpszMenuName = (char*)menuId
        };
    }

    public bool IsRegistered => Atom.IsValid || ModuleInstance.IsNull;

    public unsafe WindowClass Register()
    {
        fixed (char* name = _className)
        fixed (char* menuName = _menuName)
        {
            _wndClass.lpszClassName = name;
            if (!string.IsNullOrEmpty(_menuName))
                _wndClass.lpszMenuName = menuName;

            ATOM atom = PInvoke.RegisterClass(in _wndClass);
            if (!atom.IsValid)
            {
                throw new Win32Exception();
            }

            Atom = atom;
            return this;
        }
    }

    public IntPtr CreateWindow(
        string windowName = null,
        WINDOW_STYLE style = WINDOW_STYLE.WS_OVERLAPPED,
        WINDOW_EX_STYLE extendedStyle = default,
        bool isMainWindow = false,
        HWND parentWindow = default,
        nint parameters = default,
        HMENU menuHandle = default)
    {
        return CreateWindow(
            DefaultBounds,
            windowName,
            style,
            extendedStyle,
            isMainWindow,
            parentWindow,
            parameters,
            menuHandle);
    }

    public unsafe HWND CreateWindow(
        RECT bounds,
        string windowName = null,
        WINDOW_STYLE style = WINDOW_STYLE.WS_OVERLAPPED,
        WINDOW_EX_STYLE extendedStyle = default,
        bool isMainWindow = false,
        HWND parentWindow = default,
        nint parameters = default,
        HMENU menuHandle = default)
    {
        if (!IsRegistered)
            throw new ArgumentException("Window class must be registered before using.");

        fixed (char* wn = windowName)
        {
            HWND window;
            if (Atom.IsValid)
            {
                window = PInvoke.CreateWindowEx(
                    dwExStyle: extendedStyle,
                    lpClassName: (PCWSTR)(char*)Atom.Value,
                    lpWindowName: (PCWSTR)wn,
                    dwStyle: style,
                    X: bounds.X,
                    Y: bounds.Y,
                    nWidth: bounds.Width,
                    nHeight: bounds.Height,
                    hWndParent: parentWindow,
                    hMenu: menuHandle,
                    hInstance: (HINSTANCE)0,
                    lpParam: (void*)parameters);
            }
            else
            {
                fixed (char* cn = _className)
                {
                    window = PInvoke.CreateWindowEx(
                        dwExStyle: extendedStyle,
                        lpClassName: (PCWSTR)cn,
                        lpWindowName: (PCWSTR)wn,
                        dwStyle: style,
                        X: bounds.X,
                        Y: bounds.Y,
                        nWidth: bounds.Width,
                        nHeight: bounds.Height,
                        hWndParent: parentWindow,
                        hMenu: menuHandle,
                        hInstance: (HINSTANCE)0,
                        lpParam: (void*)parameters);
                }
            }

            if (window.IsNull)
            {
                throw new Win32Exception(Marshal.GetLastWin32Error());
            }

            if (!Atom.IsValid)
            {
                Atom = PInvoke.GetClassLong(window, GET_CLASS_LONG_INDEX.GCW_ATOM);
            }

            if (isMainWindow)
            {
                MainWindow = window;
            }

            return window;
        }
    }

    protected virtual LRESULT WNDPROC(HWND hWnd, MessageId msg, WPARAM wParam, LPARAM lParam)
    {
        switch (msg)
        {
            case PInvoke.WM_DESTROY:
                if (hWnd == MainWindow)
                {
                    PInvoke.PostQuitMessage(0);
                }

                return (LRESULT)0;
        }

        return PInvoke.DefWindowProc(hWnd, (uint)msg, wParam, lParam);
    }
}
